<?php

/**
 * Ripcord is an easy to use XML-RPC library for PHP.
 * @package Ripcord
 * @author Auke van Slooten <auke@muze.nl>
 * @copyright Copyright (C) 2010, Muze <www.muze.nl>
 * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
 * @version Ripcord 0.9 - PHP 5
 */
/**
 * Includes the static ripcord factory class and exceptions
 */
require_once(dirname(__FILE__) . '/ripcord.php');

/**
 * This class implements a simple RPC client, for XML-RPC, (simplified) SOAP 1.1 or Simple RPC. The client abstracts
 * the entire RPC process behind native PHP methods. Any method defined by the rpc server can be called as if it was
 * a native method of the rpc client.
 *
 *  E.g.
 *  <code>
 *  <?php
 *    $client = ripcord::client( 'http://www.moviemeter.nl/ws' );
 *    $score = $client->film->getScore( 'e3dee9d19a8c3af7c92f9067d2945b59', 500 );
 *  ?>
 *  </code>
 *
 * The client has a simple interface for the system.multiCall method:
 * <code>
 * <?php
 *  $client = ripcord::client( 'http://ripcord.muze.nl/ripcord.php' );
 *  $client->system->multiCall()->start();
 *  ripcord::bind( $methods, $client->system->listMethods() );
 *  ripcord::bind( $foo, $client->getFoo() );
 *  $client->system->multiCall()->execute();
 * ?>
 * </code>
 *
 * The soap client can only handle the basic php types and doesn't understand xml namespaces. Use PHP's SoapClient
 * for complex soap calls. This client cannot parse wsdl.
 * If you want to skip the ripcord::client factory method, you _must_ provide a transport object explicitly.
 *
 * @link  http://wiki.moviemeter.nl/index.php/API Moviemeter API documentation
 * @package Ripcord
 */
class Ripcord_Client {

    /**
     * The url of the rpc server
     */
    private $_url = '';

    /**
     * The transport object, used to post requests.
     */
    private $_transport = null;

    /**
     * A list of output options, used with the xmlrpc_encode_request method.
     * @see Ripcord_Server::setOutputOption()
     */
    private $_outputOptions = array(
        "output_type" => "xml",
        "verbosity" => "pretty",
        "escaping" => array("markup"),
        "version" => "xmlrpc",
        "encoding" => "utf-8"
    );

    /**
     * The namespace to use when calling a method.
     */
    private $_namespace = null;

    /**
     * A reference to the root client object. This is so when you use namespaced sub clients, you can always
     * find the _response and _request data in the root client.
     */
    private $_rootClient = null;

    /**
     * A flag to indicate whether or not to preemptively clone objects passed as arguments to methods, see
     * php bug #50282. Only correctly set in the rootClient.
     */
    private $_cloneObjects = false;

    /**
     * A flag to indicate if we are in a multiCall block. Start this with $client->system->multiCall()->start()
     */
    protected $_multiCall = false;

    /**
     * A list of deferred encoded calls.
     */
    protected $_multiCallArgs = array();

    /**
     * The exact response from the rpc server. For debugging purposes.
     */
    public $_response = '';

    /**
     * The exact request from the client. For debugging purposes.
     */
    public $_request = '';

    /**
     * Whether or not to throw exceptions when an xml-rpc fault is returned by the server. Default is false.
     */
    public $_throwExceptions = false;

    /**
     * Whether or not to decode the XML-RPC datetime and base64 types to unix timestamp and binary string
     * respectively.
     */
    public $_autoDecode = true;

    /**
     * The constructor for the RPC client.
     * @param string $url The url of the rpc server
     * @param array $options Optional. A list of outputOptions. See {@link Ripcord_Server::setOutputOption()}
     * @param object $rootClient Optional. Used internally when using namespaces.
     * @throws Ripcord_ConfigurationException (ripcord::xmlrpcNotInstalled) when the xmlrpc extension is not available.
     */
    public function __construct($url, array $options = null, $transport = null, $rootClient = null) {
        if (!isset($rootClient)) {
            $rootClient = $this;
            if (!function_exists('xmlrpc_encode_request')) {
                throw new Ripcord_ConfigurationException('PHP XMLRPC library is not installed', ripcord::xmlrpcNotInstalled);
            }
            $version = explode('.', phpversion());
            if ((0 + $version[0]) == 5) {
                if (( 0 + $version[1]) < 2) {
                    $this->_cloneObjects = true; // workaround for bug #50282
                }
            }
        }
        $this->_rootClient = $rootClient;
        $this->_url = $url;
        if (isset($options)) {
            if (isset($options['namespace'])) {
                $this->_namespace = $options['namespace'];
                unset($options['namespace']);
            }
            $this->_outputOptions = $options;
        }
        if (isset($transport)) {
            $this->_transport = $transport;
        }
    }

    /**
     * This method catches any native method called on the client and calls it on the rpc server instead. It automatically
     * parses the resulting xml and returns native php type results.
     * @throws Ripcord_InvalidArgumentException (ripcord::notRipcordCall) when handling a multiCall and the
     * arguments passed do not have the correct method call information
     * @throws Ripcord_RemoteException when _throwExceptions is true and the server returns an XML-RPC Fault.
     */
    public function __call($name, $args) {
        if (isset($this->_namespace)) {
            $name = $this->_namespace . '.' . $name;
        }

        if ($name === 'system.multiCall' || $name == 'system.multicall') {
            if (!$args || ( is_array($args) && count($args) == 0 )) {
                // multiCall is called without arguments, so return the fetch interface object
                return new Ripcord_Client_MultiCall($this->_rootClient, $name);
            } else if (is_array($args) && (count($args) == 1) &&
                    is_array($args[0]) && !isset($args[0]['methodName'])) {
                // multicall is called with a simple array of calls.
                $args = $args[0];
            }
            $this->_rootClient->_multiCall = false;
            $params = array();
            $bound = array();
            foreach ($args as $key => $arg) {
                if (!is_a($arg, 'Ripcord_Client_Call') &&
                        (!is_array($arg) || !isset($arg['methodName']) )) {
                    return false;
                }
                if (is_a($arg, 'Ripcord_Client_Call')) {
                    $arg->index = count($params);
                    $params[] = $arg->encode();
                } else {
                    $arg['index'] = count($params);
                    $params[] = array(
                        'methodName' => $arg['methodName'],
                        'params' => isset($arg['params']) ?
                                (array) $arg['params'] : array()
                    );
                }
                $bound[$key] = $arg;
            }
            $args = array($params);
            $this->_rootClient->_multiCallArgs = array();
        }
        if ($this->_rootClient->_multiCall) {
            $call = new Ripcord_Client_Call($name, $args);
            $this->_rootClient->_multiCallArgs[] = $call;
            return $call;
        }
        if ($this->_rootClient->_cloneObjects) { //workaround for php bug 50282
            foreach ($args as $key => $arg) {
                if (is_object($arg)) {
                    $args[$key] = clone $arg;
                }
            }
        }
        $request = xmlrpc_encode_request($name, $args, $this->_outputOptions);
        $response = $this->_transport->post($this->_url, $request);
        if (!$response)
            return false;
        $result = xmlrpc_decode($response);
        $this->_rootClient->_request = $request;
        $this->_rootClient->_response = $response;
        if (ripcord::isFault($result) && $this->_throwExceptions) {
            return false;
        }
        if (isset($bound) && is_array($bound)) {
            foreach ($bound as $key => $callObject) {
                if (is_a($callObject, 'Ripcord_Client_Call')) {
                    $returnValue = $result[$callObject->index];
                } else {
                    $returnValue = $result[$callObject['index']];
                }
                if (is_array($returnValue) && count($returnValue) == 1) {
                    // XML-RPC specification says that non-fault results must be in a single item array
                    $returnValue = current($returnValue);
                }
                if ($this->_autoDecode) {
                    $type = xmlrpc_get_type($returnValue);
                    switch ($type) {
                        case 'base64' :
                            $returnValue = ripcord::binary($returnValue);
                            break;
                        case 'datetime' :
                            $returnValue = ripcord::timestamp($returnValue);
                            break;
                    }
                }
                if (is_a($callObject, 'Ripcord_Client_Call')) {
                    $callObject->bound = $returnValue;
                }
                $bound[$key] = $returnValue;
            }
            $result = $bound;
        }
        return $result;
    }

    /**
     * This method catches any reference to properties of the client and uses them as a namespace. The
     * property is automatically created as a new instance of the rpc client, with the name of the property
     * as a namespace.
     * @param string $name The name of the namespace
     * @return object A Ripcord Client with the given namespace set.
     */
    public function __get($name) {
        $result = null;
        if (!isset($this->{$name})) {
            $result = new Ripcord_Client(
                    $this->_url, array_merge($this->_outputOptions, array(
                        'namespace' => $this->_namespace ?
                                $this->_namespace . '.' . $name : $name
                    )), $this->_transport, $this->_rootClient
            );
            $this->{$name} = $result;
        }
        return $result;
    }

}

/**
 * This class provides the fetch interface for system.multiCall. It is returned
 * when calling $client->system->multiCall() with no arguments. Upon construction
 * it puts the originating client into multiCall deferred mode. The client will
 * gather the requested method calls instead of executing them immediately. It
 * will them execute all of them, in order, when calling
 * $client->system->multiCall()->fetch().
 * This class extends Ripcord_Client only so it has access to its protected _multiCall
 * property.
 */
class Ripcord_Client_MultiCall extends Ripcord_Client {
    /*
     * The reference to the originating client to put into multiCall mode.
     */

    private $client = null;

    /*
     * This method creates a new multiCall fetch api object.
     */

    public function __construct($client, $methodName = 'system.multiCall') {
        $this->client = $client;
        $this->methodName = $methodName;
    }

    /*
     * This method puts the client into multiCall mode. While in this mode all
     * method calls are collected as deferred calls (Ripcord_Client_Call).
     */

    public function start() {
        $this->client->_multiCall = true;
    }

    /*
     * This method finally calls the clients multiCall method with all deferred
     * method calls since multiCall mode was enabled.
     */

    public function execute() {
        if ($this->methodName == 'system.multiCall') {
            return $this->client->system->multiCall($this->client->_multiCallArgs);
        } else { // system.multicall
            return $this->client->system->multicall($this->client->_multiCallArgs);
        }
    }

}

/**
 *  This class is used with the Ripcord_Client when calling system.multiCall. Instead of immediately calling the method on the rpc server,
 *  a Ripcord_Client_Call  object is created with all the information needed to call the method using the multicall parameters. The call object is
 *  returned immediately and is used as input parameter for the multiCall call. The result of the call can be bound to a php variable. This
 *  variable will be filled with the result of the call when it is available.
 * @package Ripcord
 */
class Ripcord_Client_Call {

    /**
     * The method to call on the rpc server
     */
    public $method = null;

    /**
     * The arguments to pass on to the method.
     */
    public $params = array();

    /**
     * The index in the multicall request array, if any.
     */
    public $index = null;

    /**
     * A reference to the php variable to fill with the result of the call, if any.
     */
    public $bound = null;

    /**
     * The constructor for the Ripcord_Client_Call class.
     * @param string $method The name of the rpc method to call
     * @param array $params The parameters for the rpc method.
     */
    public function __construct($method, $params) {
        $this->method = $method;
        $this->params = $params;
    }

    /**
     * This method allows you to bind a php variable to the result of this method call.
     * When the method call's result is available, the php variable will be filled with
     * this result.
     * @param mixed $bound The variable to bind the result from this call to.
     * @return object Returns this object for chaining.
     */
    public function bind(&$bound) {
        $this->bound = & $bound;
        return $this;
    }

    /**
     * This method returns the correct format for a multiCall argument.
     * @return array An array with the methodName and params
     */
    public function encode() {
        return array(
            'methodName' => $this->method,
            'params' => (array) $this->params
        );
    }

}

/**
 * This interface describes the minimum interface needed for the transport object used by the
 * Ripcord_Client
 * @package Ripcord
 */
interface Ripcord_Transport {

    /**
     * This method must post the request to the given url and return the results.
     * @param string $url The url to post to.
     * @param string $request The request to post.
     * @return string The server response
     */
    public function post($url, $request);
}

/**
 * This class implements the Ripcord_Transport interface using PHP streams.
 * @package Ripcord
 */
class Ripcord_Transport_Stream implements Ripcord_Transport {

    /**
     * A list of stream context options.
     */
    private $options = array();

    /**
     * Contains the headers sent by the server.
     */
    public $responseHeaders = null;

    /**
     * This is the constructor for the Ripcord_Transport_Stream class.
     * @param array $contextOptions Optional. An array with stream context options.
     */
    public function __construct($contextOptions = null) {
        if (isset($contextOptions)) {
            $this->options = $contextOptions;
        }
    }

    /**
     * This method posts the request to the given url.
     * @param string $url The url to post to.
     * @param string $request The request to post.
     * @return string The server response
     * @throws Ripcord_TransportException (ripcord::cannotAccessURL) when the given URL cannot be accessed for any reason.
     */
    public function post($url, $request) {
        $options = array_merge(
                $this->options, array(
            'http' => array(
                'method' => "POST",
                'header' => "Content-Type: text/xml",
                'content' => $request
            )
                )
        );
        $context = stream_context_create($options);
        $result = @file_get_contents($url, false, $context);
        $this->responseHeaders = $http_response_header;
        if (!$result) {
            print 'Could not access ' . $url ." E:". ripcord::cannotAccessURL . "\n";
            return false;
        }
        return $result;
    }

}

/**
 * This class implements the Ripcord_Transport interface using CURL.
 * @package Ripcord
 */
class Ripcord_Transport_CURL implements Ripcord_Transport {

    /**
     * A list of CURL options.
     */
    private $options = array();

    /**
     * A flag that indicates whether or not we can safely pass the previous exception to a new exception.
     */
    private $skipPreviousException = false;

    /**
     * Contains the headers sent by the server.
     */
    public $responseHeaders = null;

    /**
     * This is the constructor for the Ripcord_Transport_CURL class.
     * @param array $curlOptions A list of CURL options.
     */
    public function __construct($curlOptions = null) {
        if (isset($curlOptions)) {
            $this->options = $curlOptions;
        }
        $version = explode('.', phpversion());
        if (( (0 + $version[0]) == 5) && ( 0 + $version[1]) < 3) { // previousException supported in php >= 5.3
            $this->_skipPreviousException = true;
        }
    }

    /**
     * This method posts the request to the given url
     * @param string $url The url to post to.
     * @param string $request The request to post.
     * @throws Ripcord_TransportException (ripcord::cannotAccessURL) when the given URL cannot be accessed for any reason.
     * @return string The server response
     */
    public function post($url, $request) {
        $curl = curl_init();
        $options = (array) $this->options + array(
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $request,
            CURLOPT_HEADER => true
        );
        curl_setopt_array($curl, $options);
        $contents = curl_exec($curl);
        $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
        $this->responseHeaders = substr($contents, 0, $headerSize);
        $contents = substr($contents, $headerSize);

        if (curl_errno($curl)) {
            $errorNumber = curl_errno($curl);
            $errorMessage = curl_error($curl);
            curl_close($curl);
            $version = explode('.', phpversion());
        }
        curl_close($curl);
        return $contents;
    }

}

?>